package com.zyf.chapter01.refactor.practice.third;

import com.zyf.chapter01.refactor.practice.VisibleForTesting;
import com.zyf.chapter01.refactor.practice.first.IdGenerator;
import com.zyf.chapter01.refactor.util.exception.IdGenerationFailureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Random;

/**
 * 第三重构
 * 处理了一下问题
 *     重构各个函数的异常处理代码
 * 增加了可测试性
 */
public class RandomIdGenerator implements IdGenerator {

    private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);

    @Override
    public String generate() throws IdGenerationFailureException {
        /**
         * 对于以下方法的异常，我们选择将异常封装，而不是直接抛出，原因如下
         *   调用者并不关心 ID 是如何生成的，要依赖抽象而非实现编程，如果直接抛出
         * UnknownHostException，实际上是暴露了细节，
         *   我们不希望将 UnknownHostException 这个比较底层的异常，暴露给上层
         * 代码，调用者拿到这个异常，并不能理解该异常代表什么，也不知该如何处理。
         *   UnknownHostException 异常跟 generate() 函数，在业务概念上没有
         * 相关性。
         */
        String substrOfHostName = null;
        try {
            substrOfHostName = getLastfieldOfHostName();
        } catch (UnknownHostException e) {
            throw new IdGenerationFailureException("host name is empty.");
        }
        /**
         * 如果可以接受 generate 函数返回
         * null-16723733647-83Ab3uK6
         * 或者返回
         * -16723733647-83Ab3uK6
         * 则也可不用抛出异常
         */
        // 该做法更倾向于明确将异常告知调用者，且异常为受检异常
        if (substrOfHostName == null || substrOfHostName.isEmpty()) {
            throw new IdGenerationFailureException("host name is empty.");
        }
        long currentTimeMillis = System.currentTimeMillis();
        String randomString = generateRandomAlphameric(8);
        String id = String.format("%s-%d-%s",
                substrOfHostName, currentTimeMillis, randomString);
        return id;
    }

    /**
     * 以{@code .}为分隔调节，获取最后一段
     * @return 获取 ip 的最后一串数字
     */
    private String getLastfieldOfHostName() throws UnknownHostException {
        String substrOfHostName = null;

        String hostName = InetAddress.getLocalHost().getHostName();
        // 由于 getLastSubstrSplittedByDot 方法做了 null 判断，
        // 所以在这一层自己也要保证不传递 NULL 或空字符串。
        if (hostName==null || hostName.isEmpty()) {
            throw new UnknownHostException("...");
        }
        // 将逻辑分离出来以便测试
        substrOfHostName = getLastSubstrSplittedByDot(hostName);
        return substrOfHostName;
            /**
             * 当主机名获取失败，则该方法会返回 NULL，对我们来说，NULL 会影响后续逻辑
             * 所以这是异常行为，最好抛出。
             * 是直接抛出 UnknownHostException 还是重新封装该异常？
             * 该异常标识主机名获取失败，算是业务相关，所以不需要包裹。
             */
//        } catch (UnknownHostException e) {
//            logger.warn("Failed to get the host name.", e);
//        }
    }

    @VisibleForTesting
    protected String getLastSubstrSplittedByDot(String hostName) {
        /**
         * 如果函数是私有的，完全在自己的掌控之下，自己包装调用时，不传递 NULLL 值
         * 或空字符串即可。
         * 但是，如果是其他 public，则无法掌控被谁调用，这是，为了提高代码的健壮性，
         * 最好是在 public 函数中做 NULL 值或空字符串的判断。
         * 该方法中加 if 的原因
         *   单元测试要测试 corner case
         */
        if (hostName == null || hostName.isEmpty()) {
            // 运行时异常，非受检异常
            throw new IllegalArgumentException("..");
        }
        String[] tokens = hostName.split("\\.");
        String substrOfHostName = tokens[tokens.length - 1];
        return substrOfHostName;
    }

    /**
     * 将随机生成字母数字串的逻辑分离了处理
     * 处理了魔术的问题
     * 处理了多个 if 的问题
     * @param length 字母数字串的长度，
     * @return 随机生成的字母和数字串
     * @throws IllegalArgumentException 如果 {@code length <= 0}
     */
    @VisibleForTesting
    protected String generateRandomAlphameric(int length) {
        /**
         * length < 0，是不符合常规逻辑的，是一种异常行为。
         * length = 0，是否为正常行为得看自己定义，可以抛异常，也可以返回空字符串
         *   无论选择哪一种，都应在函数注释中著名
         */
        if (length <= 0) {
            throw new IllegalArgumentException("...");
        }
        char[] randomChars = new char[length];
        int count = 0;
        Random random = new Random();
        while (count < length) {
            int maxAscii = 'z';
            int randomAscii = random.nextInt(maxAscii);
            boolean isDigit = randomAscii >= '0' && randomAscii <= '9';
            boolean isUppercase = randomAscii >= 'A' && randomAscii <= 'Z';
            boolean isLowercase = randomAscii >= 'a' && randomAscii <= 'z';
            if (isDigit || isUppercase || isLowercase) {
                randomChars[count++] = (char) randomAscii;
            }
        }
        return new String(randomChars);
    }
}
